/**
 * 
 */
package three_star_craft;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.grlea.log.SimpleLogger;

import ProtocolsCommon.Common.IpAddress;
import ProtocolsCommon.Common.PlayableItem;
import ProtocolsCommon.Common.PlayableItem.ItemType;
import UserApplicationPart.UAP;
import UserApplicationPart.UAP.UapConnectionConfirm;
import UserApplicationPart.UAP.UapConnectionReject;
import UserApplicationPart.UAP.UapConnectionReleaseCfm;
import UserApplicationPart.UAP.UapConnectionReleaseReq;
import UserApplicationPart.UAP.UapConnectionRequest;
import UserApplicationPart.UAP.UapMediaBearerSetupConfirm;
import UserApplicationPart.UAP.UapMediaBearerSetupReject;
import UserApplicationPart.UAP.UapMediaBearerSetupRequest;
import UserApplicationPart.UAP.UapPdu;
import UserApplicationPart.UAP.UapPduNumber;

import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;

/**
 * Stub representing serving gateway.
 * 
 * @author w.antosiewic
 *
 */
public class SgwStub
{
    private static final SimpleLogger logger = new SimpleLogger(SgwStub.class);

    private ServerSocket uapSocket;
    private ServerSocket mediaServerSocket;

    public static int SGW_UAP_PORT = 9696;
    public static int SWG_MEDIA_PORT = 21669;

    private UapServerThread uapServerThread;

    private SocketThread uapSocketThread;

    private MediaServerThread mediaServerThread;
    
    private List<Socket> sockets = new ArrayList<Socket>();
    private List<Socket> mediaSockets = new ArrayList<Socket>();

    private Map<Integer, PlayableItem> _mediaList = 
            new HashMap<Integer, PlayableItem>();
    
    private File mediaFile;
    
    public SgwStub()
    {
        try
        {
            uapSocket = new ServerSocket(SGW_UAP_PORT);
            logger.debugObject("Creating server socket on port", uapSocket.getLocalPort());
            uapServerThread = new UapServerThread();
            uapServerThread.start();

            uapSocketThread = new SocketThread();
            uapSocketThread.start();
            
            mediaServerSocket = new ServerSocket(SWG_MEDIA_PORT);
            logger.debugObject("Creating media server socket on port", mediaServerSocket.getLocalPort());
            mediaServerThread = new MediaServerThread();
            mediaServerThread.start();
        } catch (IOException e)
        {
            logger.errorException(e);
        } 
        
        _mediaList.put(new Integer(0), // id
                PlayableItem.newBuilder()
                    .setId(0)
                    .setAuthor("Dream Theater")
                    .setName("Erotomania")
                    .setSize(71322664)
                    .setType(ItemType.TYPE_AUDIO)
                .build());
        
        _mediaList.put(new Integer(1), // id
                PlayableItem.newBuilder()
                    .setAuthor("Author 1")
                    .setId(1)
                    .setName("Name 2")
                    .setSize(127896)
                    .setType(ItemType.TYPE_AUDIO)
                .build());
        
        _mediaList.put(new Integer(2), // id
                PlayableItem.newBuilder()
                    .setAuthor("Author 44")
                    .setId(2)
                    .setName("Some name")
                    .setSize(99556600)
                    .setType(ItemType.TYPE_AUDIO)
                .build());
        
//        mediaFile = this.getClass().getClassLoader().getResource("").g;
    }

    public InetAddress getAddress()
    {
        return uapSocket.getInetAddress();
    }

    public static void main(String[] args)
    {
        logger.debug("Starting SgwStub as standalone application");
        new SgwStub();
    }

    public void closeAll()
    {
        for(Socket s : sockets)
        {
            if(!s.isClosed())
            {
                logger.debugObject("unbindAll closing socket:", s);
                try
                {
                    s.close();
                } catch (IOException e)
                {
                    logger.errorException(e);
                }
            }
        }
    }

    void handleUapPdu(UapPdu pdu, Socket s)
    {
        logger.debugObject("handling UapPdu with pduNum=", pdu.getPduNumber());
        switch(pdu.getPduNumber())
        {
            case UAP_PDU_CONNECTION_REQ:
            {
                logger.debugObject("Received UapConnectionReq from socket:", s);
                UapConnectionRequest connReq;
                try
                {
                    connReq = UapConnectionRequest.parseFrom(pdu.getPdu());
                } catch (InvalidProtocolBufferException e)
                {
                    logger.errorException(e);
                    sendUapConnectionReject(pdu.getConnectionId(), UAP.Cause.CAUSE_SYNTAX_ERROR, s);
                    return;
                }
                if(!connReq.isInitialized())
                {
                    logger.error("Received invalid UapConnectionReq, errorStr:"+
                            connReq.getInitializationErrorString());
                    sendUapConnectionReject(pdu.getConnectionId(), UAP.Cause.CAUSE_SYNTAX_ERROR, s);
                    return;
                }
                handleUapConnectionReq(pdu.getConnectionId(), connReq, s);
            }
            break;
            
            case UAP_PDU_CONNECTION_RELEASE_REQ:
            {
                logger.debugObject("Received UapConnectionReleaseReq from socket:", s);
                UapConnectionReleaseReq relReq;
                try
                {
                    relReq = UapConnectionReleaseReq.parseFrom(pdu.getPdu());
                } catch (InvalidProtocolBufferException e)
                {
                    logger.errorException(e);
                    return;
                }
                if(!relReq.isInitialized())
                {
                    logger.error("Received invalid UapConnectionReleaseReq, errorStr:"+
                            relReq.getInitializationErrorString());
                    return;
                }
                handleUapConnectionReleaseReq(pdu.getConnectionId(), relReq, s);
            }
            break;
            
            case UAP_PDU_MEDIA_BEARER_SETUP_REQ:
            {
                logger.debugObject("Received UapMediaBearerReq from socket:", s);
                UapMediaBearerSetupRequest mediaBearerReq;
                try
                {
                    mediaBearerReq = UapMediaBearerSetupRequest.parseFrom(pdu.getPdu());
                } catch (InvalidProtocolBufferException e)
                {
                    logger.errorException(e);
                    sendUapMediaBearerSetupRej(pdu.getConnectionId(), UAP.Cause.CAUSE_SYNTAX_ERROR, s);
                    return;
                }
                if(!mediaBearerReq.isInitialized())
                {
                    logger.error("Received invalid UapMediaBearerReq, errorStr:"+
                            mediaBearerReq.getInitializationErrorString());
                    sendUapMediaBearerSetupRej(pdu.getConnectionId(), UAP.Cause.CAUSE_SYNTAX_ERROR, s);
                    return;
                }
                handleUapMediaBearerReq(pdu.getConnectionId(), mediaBearerReq, s);
            }
            break;
            
            default:
                logger.warn("Received unexpected UapPdu with number: "+pdu.getPduNumber());
        }
    }

    void handleUapConnectionReq(int connId, UapConnectionRequest connReq, Socket s)
    {
        UapConnectionConfirm.Builder connCfmBuilder = UapConnectionConfirm.newBuilder();
        for(PlayableItem mediaItem : _mediaList.values())
        {
            connCfmBuilder.addItems(mediaItem);
        }
        
        UapConnectionConfirm connCfm = connCfmBuilder.build();
        if(!connCfm.isInitialized())
        {
            logger.warn("UapConnectionCfm is not initialized:"+connCfm.getInitializationErrorString());
            return;
        }
        logger.debugObject("Sending UapConnectionCfm size="+connCfm.getSerializedSize()+", on socket:", s);
        sendUapPdu(connId, UapPduNumber.UAP_PDU_CONNECTION_CFM, connCfm.toByteString(), s);
    }

    void sendUapConnectionReject(int connId, UAP.Cause cause, Socket s)
    {
        UapConnectionReject.Builder rejBuilder = UapConnectionReject.newBuilder();
        rejBuilder.setCause(cause);

        logger.debugObject("Sending UapConnectionRej on socket:", s);
        logger.verboseObject("UapConnRej:", rejBuilder);

        UapConnectionReject connRej = rejBuilder.build();
        sendUapPdu(connId, UapPduNumber.UAP_PDU_CONNECTION_REJ, connRej.toByteString(), s);
    }

    void handleUapConnectionReleaseReq(int connId, UapConnectionReleaseReq relReq, Socket s)
    {
        UapConnectionReleaseCfm.Builder relCfmBuilder = UapConnectionReleaseCfm.newBuilder();

        UapConnectionReleaseCfm relCfm = relCfmBuilder.build();
        if(!relCfm.isInitialized())
        {
            logger.warn("UapConnectionReleaseCfm is not initialized:"+
                    relCfm.getInitializationErrorString());
            return;
        }
        logger.debugObject("Sending UapConnectionReleaseCfm size="+relCfm.getSerializedSize()+", on socket:", s);
        sendUapPdu(connId, UapPduNumber.UAP_PDU_CONNECTION_RELEASE_CFM, relCfm.toByteString(), s);
    }
    
    void handleUapMediaBearerReq(int connId, UapMediaBearerSetupRequest mediaBearerReq, Socket s)
    {
        // find item with ID specified in the request
        PlayableItem media = _mediaList.get(mediaBearerReq.getMediaItemId());
        if(media == null)
        {
            sendUapMediaBearerSetupRej(connId, UAP.Cause.CAUSE_MEDIA_NOT_AVAILABLE, s);
        }
        UapMediaBearerSetupConfirm.Builder mediaBearerCfmBuilder = 
                UapMediaBearerSetupConfirm.newBuilder();
        
        mediaBearerCfmBuilder.setServerMediaPort(SWG_MEDIA_PORT);
        IpAddress.Builder mediaIpAddressBuilder = IpAddress.newBuilder();
        mediaIpAddressBuilder.setIpBytes(
                ByteString.copyFrom(
                        mediaServerSocket.getInetAddress().getAddress()));
        mediaBearerCfmBuilder.setServerMediaAddress(mediaIpAddressBuilder);
        
        if(!mediaBearerCfmBuilder.isInitialized())
        {
            logger.warn("UapMediaBearerSetupConfirm is not initialized:"+
                    mediaBearerCfmBuilder.getInitializationErrorString());
            return;
        }
        UapMediaBearerSetupConfirm mediaBearerCfm = mediaBearerCfmBuilder.build();
        logger.debugObject("Sending UapMediaBearerSetupConfirm size="+
                mediaBearerCfm.getSerializedSize()+", on socket:", s);
        
        sendUapPdu(connId,
                   UapPduNumber.UAP_PDU_MEDIA_BEARER_SETUP_CFM,
                   mediaBearerCfm.toByteString(),
                   s);
    }
    
    void sendUapMediaBearerSetupRej(int connId, UAP.Cause cause, Socket s)
    {
        UapMediaBearerSetupReject.Builder mediaSetupRejBuilder = 
                UapMediaBearerSetupReject.newBuilder();
        mediaSetupRejBuilder.setCause(cause);
        
        UapMediaBearerSetupReject mediaSetupRej = mediaSetupRejBuilder.build();
        if(!mediaSetupRej.isInitialized())
        {
            logger.warn("UapMediaBearerSetupRej is not initialized:"+
                    mediaSetupRej.getInitializationErrorString());
            return;
        }
        logger.debugObject("Sending UapMediaBearerSetupRej size="+
                mediaSetupRej.getSerializedSize()+", on socket:", s);
        sendUapPdu(connId, 
                   UapPduNumber.UAP_PDU_MEDIA_BEARER_SETUP_REJ,
                   mediaSetupRej.toByteString(),
                   s);
    }
    
    private boolean sendUapPdu(int connId,
                               UapPduNumber pduNumber,
                               ByteString bytes,
                               Socket s)
    {
        try
        {
            UapPdu uapPdu = UapPdu.newBuilder()
                    .setPduNumber(pduNumber)
                    .setPdu(bytes)
                    .setConnectionId(connId)
                    .build();

            uapPdu.writeTo(s.getOutputStream());
            s.getOutputStream().flush();
            logger.verboseObject("UAP_BUS_SEND: "+pduNumber+",size="+
                    uapPdu.getSerializedSize(), uapPdu.toByteArray());
        } catch (IOException e)
        {
            logger.errorException(e);
            return false;
        }

        return true;
    }

    class UapServerThread extends Thread
    {
        public void run()
        {
            while(true)
            {
                try
                {
                    Socket newSocket = uapSocket.accept();
                    logger.debugObject("new Socket created, remote port="+newSocket.getPort()+", remote address=", newSocket.getInetAddress());
                    synchronized(sockets)
                    {
                        sockets.add(newSocket);
                    }
                } catch (IOException e)
                {
                    logger.errorException(e);
                }
            }
        }
    }

    class SocketThread extends Thread
    {
        public void run()
        {
            while(true)
            {
                synchronized (sockets)
                {

                    for(Socket s : sockets)
                    {
                        logger.ludicrousObject("run - handling socket", s);
                        if(s.isConnected())
                        {
                            logger.ludicrous("Socket is connected trying to read data");
                            try
                            {
                                InputStream is = s.getInputStream();
                                int numBytes = is.available(); 
                                if(numBytes > 0)
                                {
                                    logger.debugObject("Socket: "+s+", has available bytes", numBytes);
                                    byte[] buf = new byte[numBytes];
                                    int readBytes = is.read(buf);
                                    logger.debugObject("Read bytes["+readBytes+"]:", buf);

                                    UapPdu uapPdu = UapPdu.parseFrom(buf);
                                    if(!uapPdu.isInitialized())
                                    {
                                        logger.error("Received invalid UapPdu, errorString:"+uapPdu.getInitializationErrorString());
                                    }
                                    handleUapPdu(uapPdu, s);
                                }
                            } catch (IOException e)
                            {
                                logger.errorException(e);
                            }

                        }
                        else
                        {
                            logger.verbose("Socket is not connected");
                        }
                    }
                }
                try
                {
                    Thread.sleep(50);
                } catch (InterruptedException e)
                {
                    logger.errorException(e);
                }
            }
        }
    }
    
    class MediaServerThread extends Thread
    {
        public void run()
        {
            while(true)
            {
                try
                {
                    Socket newSocket = mediaServerSocket.accept();
                    logger.debugObject("new media socket created, remote port="+
                            newSocket.getPort()+", remote address=", newSocket.getInetAddress());
                    synchronized(mediaSockets)
                    {
                        mediaSockets.add(newSocket);
                    }
                } catch (IOException e)
                {
                    logger.errorException(e);
                }
            }
        }
    }
    
}
